/**
 * This namespace defines selector for JSON objects, which is a simplified Xpath for
 * safe JSON object data selection. The functions in this namesspace are used within 
 * <code>{@link jsMotif.Template}</code>.
 * @namespace
 * @publish
 */
jsMotif.selector = (function(){
     
     /**
      * Trim
      * @private
      * @param {Object} text
      */
     function trim (text) {
        return text.replace(/(^\s+)|(\s+$)/g, '');
     }
     
     /**
      * Unquote
      * @param {Object} text
      */
     function unquote(text){
         return text.replace(/(^\')|(^\")|(\'$)|(\"$)/g, '');
     }
    
    /**
     * Filtering operatos
     * 
     * @private
     * @param {Object} argLeft
     * @param {Object} argRight
     */
     var operators = {
         '==': function(argLeft, argRight){
             return argLeft == argRight;
         },
         '!=': function (argLeft, argRight){
             return (argLeft)? argLeft != argRight : false;
         },
         '>': function (argLeft, argRight){
             return argLeft > argRight;
         },
         '<': function (argLeft, argRight){
             return argLeft < argRight;
         },
         '>=': function (argLeft, argRight){
             return argLeft >= argRight;
         },
         '<=': function (argLeft, argRight){
             return argLeft <= argRight;
         },
         '!': function (argLeft, argRight){
             return !argLeft;
         }
     };
     
     /**
      * Function parses filter selector, to set the action 
      * function for the selector operator
      * 
      * @private
      * @param {Object} selector
      */
     function parseSelector(selector) {
         
         var selectedIndex = -1,
             selectedOperator,
             index,
             argLeft,
             argRight,
             action;
         
         if(selector[0] === '!'){
             action = operators['!'];
             argLeft = trim(selector.substr(1, selector.length));
         }else{
            for(var operator in operators){
                if (operators.hasOwnProperty(operator)) {
                    index = selector.lastIndexOf(operator);
                    if (index > selectedIndex) {
                        selectedIndex = index;
                        selectedOperator = operator;
                    }
                }
            }
            
            if (selectedIndex !== -1) {
                 action = operators[selectedOperator];
                 argRight = unquote(trim(selector.substr(selectedIndex, selector.length).split(selectedOperator)[1]));
                 argLeft = (selectedOperator !== '!')? trim(selector.substr(0, selectedIndex)) : argRight;
            }
         }

        return {
            valuePath: argLeft,
            toValue: argRight,
            action: action
        };  
          
     }
     
     
     
     
     /**
      * Function gets property for an object, 
      * property is stored as an array of characters
      * 
      * @private
      * @param {Object} obj
      * @param {Object} subPath
      */
     function getProperty(obj, subPath) {
         var property = subPath.join('');
         if(property!==''){
            obj = obj[property];
         }
         
         return obj;
     }
     
    
     /**
      * Functions for json object path tokens
      * handling. On each token action is performed
      * 
      * @private
      * @param {Object} obj
      * @param {Object} subPath
      * @param {Object} stack
      * @param {Object} token
      */
     var tokens = {
         '.': function(obj, subPath, stack, token){
             if (!stack.length) {
                 if (obj instanceof Array) {
                    for(var i=0, l=obj.length, result = []; i<l; i++){
                        result.push(getProperty(obj[i],subPath));
                    }
                    
                    obj = result;
                 }
                 else {
                     obj = getProperty(obj, subPath);
                 }
                 
                 subPath.splice(0, subPath.length);
             }else{
                 subPath.push(token);
             }
             
             return obj;
         },
         
         '(': function (obj, subPath){
             return obj;
         },
         
         ')': function (obj, subPath, stack, token) {
             if (!stack.length) {
                 var property = subPath.join('');
                 if (typeof obj[property] === 'function') {
                     return obj[property]();
                 }
                 return;
             }else{
                 subPath.push(token);
             }
             return obj;
         },
         
         '[': function (obj, subPath, stack, token){
            
             if(!stack.length){
                 obj = getProperty(obj, subPath);
                 subPath.splice(0, subPath.length);
             }else{
                 subPath.push(token);
             }
             
             stack.push(token);
             return obj;
         },
         
         ']': function (obj, subPath, stack, token) {
             stack.pop();
             if(!stack.length){
                 var property = subPath.join('');
                 if(isNaN(property)){
                     obj = getBySelector(obj, property);
                 }else{
                     if (property !== '') {
                         obj = obj[parseInt(property, 10)];
                     }
                 }
                 subPath.splice(0, subPath.length);
             }else{
                 subPath.push(token);
             }
             
             return obj;
         }
     };
   
   
     /**
      * Normalize path, normalizes path removes extra '.', and '"' and adds
      * '.' at the end of the path
      * @private
      * @param {Object} path
      */
     function normalizePath (path) {
        path = (typeof(path) === 'string')? path : '';
        path = path.replace(/([\]\)\.]+)\./g, function($0, $1){
            return $1 ? $1 + '' : $0;
        });
        
        if(!tokens[path.charAt(path.length-1)]){
            path += '.';
        }
        
        return path;
     }
    
    
    /**
     * This function retrieves data from a JSON object, using the provided path. 
     *
     * @param {Object} path The path to data within the JSON object; may contain array selectors
     * @param {Object} defaultValue The default value to return when the path finds nothing
     * 
     * @publish
     * 
     * @example 
     * getData.call(jsonObject,'path1.path2[3]','default text');
     * 
     */
    function getData(path, defaultValue) {
        var obj = this,
            subPath = [],
            stack = []; 
        
        path = normalizePath(path);
        for(var i=0, l=path.length; i<l; i++){
            var ch = path.charAt(i);
            
            if(tokens[ch]){
               obj = tokens[ch](obj, subPath, stack, ch);
            }else{
               subPath.push(ch);
            }
            
            if(!obj){
                return defaultValue;
            }
        }
        return obj;
       
    }
    
     /**
      * This function resolves a conditional path or a selector.
      * 
      * @param {Object} selector A selector or a conditional path ({String}) 
      * @return A value (data) selected by the argument or, if the argument contains a conditional 
      * path, a Boolean value indicating if the condition is met
      * @publish
      * 
      * @example 
      * resolveCondition.call(jsonObj,'path1.inner1');
      * resolveCondition.call(jsonObj,'path1.inner2=="some text");
      */
     function resolveCondition (selector){
        var obj = this,
            select,
            value,
            data = getData.call(obj, selector, false);
        
        if(data){
            return data;
        }else{
            select = parseSelector(selector);
            if(select.action){
                value = getData.call(obj, select.valuePath);
                return select.action(value, select.toValue);
            }
        }
    
     }
    
    
    /**
      * Function gets elements from the array according to 
      * selector
      * 
      * @private
      * @param {Object} obj
      * @param {Object} selector
      */
     function getBySelector (obj, selector) {
         var select = parseSelector(selector),
             results = [];
        
         for(var i=0, l=obj.length; i<l; i++){
             var arrayObj = obj[i],
                 value = getData.call(arrayObj, select.valuePath);
                 
             if(select.action(value, select.toValue)){
                 results.push(arrayObj);
             }
         }
        
         return results;
     }
    
    return {
        resolveCondition: resolveCondition,
        getData: getData
    };
    
}());
